package events;

import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.Map.Entry;

import javax.management.InvalidAttributeValueException;

import eduni.simjava.Sim_entity;
import eduni.simjava.Sim_event;
import eduni.simjava.Sim_port;
import eduni.simjava.Sim_stat;
import eduni.simjava.Sim_system;

public class ServerPool extends Sim_entity{
        
        private HashMap<String, CommonServer> servers;
        
        private Sim_port in;// Port that receives requests from clients
        private HashMap<CommonServer, Sim_port> serverPorts;//Ports to connect with each server
        private Sim_port outToPoolAgain;// Port that schedule another arrival for busy servers
        private Sim_stat stat;
        
        public static ServerPool instance = null;
        
        private ServerPool(String name){
        	super(name);
        	
            servers = new HashMap<String, CommonServer>();
            serverPorts = new HashMap<CommonServer, Sim_port>();
            
            in = new Sim_port("in");
    		add_port(in);
    		
    		outToPoolAgain = new Sim_port("outToPool");
    		add_port(outToPoolAgain);
    		
    		//Starting statistics of the pool
    		stat = new Sim_stat();
            stat.add_measure(Sim_stat.THROUGHPUT);
            stat.add_measure(Sim_stat.ARRIVAL_RATE);
            stat.add_measure(Sim_stat.QUEUE_LENGTH);
            stat.add_measure(Sim_stat.SERVICE_TIME);
            stat.add_measure(Sim_stat.UTILISATION);
            stat.add_measure(Sim_stat.WAITING_TIME);
            set_stat(stat);
        }
        
        private ServerPool(){
        	this("ServerPool");
        }
        
        public static ServerPool getInstance(){
        	if (instance == null){
        		instance = new ServerPool();
        	}
        	
        	return instance;
        }
        
        /**
         * This method will set the ports that will be used to communicate with each server
         * @param serverPoolOutPorts
         * @throws InvalidAttributeValueException
         */
        public void setServerPorts(HashMap<CommonServer, Sim_port> serverPoolOutPorts) throws InvalidAttributeValueException{
			if (serverPoolOutPorts == null || serverPoolOutPorts.size() == 0){
        		throw new InvalidAttributeValueException("Should give ports for the server pool!!!");
        	}
			
			for(Sim_port port : serverPoolOutPorts.values()){
				add_port(port);
			}
			
			this.serverPorts = serverPoolOutPorts;
        }
        
        /**
         * This method will add a new server to the pool
         * @param server
         */
        public void addServer(CommonServer server){
            if (server == null){
                throw new InvalidParameterException("Should give a server different of null to the pool!!");
            }
            
            servers.put(server.getServerName(), server);
        }
        
        @Override
        public void body(){
        	while (Sim_system.running()) {

        		Sim_event e = new Sim_event();
    			// Get the next event
    			sim_get_next(e);

    			if (e.get_tag() == EventType.REQUEST_FILE) {
    				
    				Client client = (Client) e.get_data();
    				String fileBeingDownloaded = client.getFileBeingDownloaded();
    				int blockBeingRequested = client.getBlockBeingRequested();
    				long amountToDownload = client.getAmountToDownload();
    				
    				//Searching for an available server that has the block requested
    				CommonServer availableServer = findAvailableServer(fileBeingDownloaded, blockBeingRequested, client);
    				
    				// Process the event
    				sim_process(0.0);
    				
    				// The event has completed service
    				sim_completed(e);
    				
    				if(availableServer == null){//Scheduling event for arrival in future
    					double nextFreeTime = findNextAvailableTime(fileBeingDownloaded, blockBeingRequested);
    					double delay = nextFreeTime - Sim_system.clock();
    					if(delay < 0){
    						delay = 0;
    					}
    					
    					sim_schedule(outToPoolAgain, delay, EventType.REQUEST_FILE, client);
    				}else{//A server is available to be requested
    					Sim_port sim_port = serverPorts.get(availableServer);
    					sim_schedule(sim_port, 0.0, EventType.REQUEST_FILE, client);
    				}
    			}
        	}
        }
        
        private double findNextAvailableTime(String fileBeingDownloaded, int blockBeingRequested) {
			double closerNextAvailableTime = Double.MAX_VALUE;
			
			for (CommonServer server : servers.values()){
				if (server.hasFileAvailable(fileBeingDownloaded, blockBeingRequested)){
					double nextFreeTime = server.getNextFreeTime();
					if (nextFreeTime < closerNextAvailableTime){
						closerNextAvailableTime = nextFreeTime;
					}
				}
			}
        	
        	return closerNextAvailableTime;
		}

		/**
         * This method will search for an available server in the servers that exists in order to supply
         * the client demand 
         * @param fileBeingDownloaded
         * @param blockBeingRequested
         * @param client
         * @return
         */
		private synchronized CommonServer findAvailableServer(String fileBeingDownloaded, int blockBeingRequested, Client client) {
			String clientName = client.get_name();
			String modifiedName = clientName.replaceAll("Client", "");
			
			//Checking first the principal server
			CommonServer commonServer = servers.get("Server");
			boolean hasFile = commonServer.hasFileAvailable(fileBeingDownloaded, blockBeingRequested);
			if (hasFile && !commonServer.isBusy()){
				return commonServer;
			}	
			
			//Checking other servers
			for(Entry<String, CommonServer> entry : servers.entrySet()){
				String serverName = entry.getKey();
				CommonServer server = entry.getValue();
				
				if(serverName.matches(".*"+modifiedName)){// A client cannot ask data from itself
					continue;
				}
				
				hasFile = server.hasFileAvailable(fileBeingDownloaded, blockBeingRequested);
				if (hasFile && !server.isBusy()){
					return server;
				}
			}
			
			return null;
		}

		/**
		 * This method will update the data related to the files, as well as the blocks,
		 * that are available at each server
		 * @param clientName
		 * @param fileBeingRequested
		 * @param blockBeingRequested
		 * @throws InvalidAttributeValueException
		 */
		public synchronized void updateServerData(String clientName, String fileBeingRequested, int blockBeingRequested) throws InvalidAttributeValueException {
			String modifiedName = clientName.replaceAll("Client", "");
			String serverName = "ClientServer"+modifiedName;
			
			CommonServer commonServer = servers.get(serverName);
			if(commonServer == null){
				return;
			}
			
			commonServer.updateFileAvailableData(fileBeingRequested, blockBeingRequested);
		}
        
//        public CommonServer findAvailableServer(){
//            for (CommonServer server : servers){
//                    if(!server.isBusy()){
//                            return server;
//                    }
//            }
//            
//            return null;
//        }
//        
//        public void addClientToQueue(Client client){
//                if (serverPoolQueue.size() < queueSize){
//                        this.serverPoolQueue.add(client);
//                }else{
//                        clientsMissedAtServer++;
//                }
//        }
//        
//        public void removeClientFromQueue(Client client){
//                serverPoolQueue.remove(client);
//        }

}
